ElasticSearch 聚合查询语法

参考资料:https://www.elastic.co/guide/cn/elasticsearch/guide/current/aggregations.html

创建数据

我们先通过模拟的数据来看看有哪些语法。

由于es集群禁止了自动创建索引,我们只能先创建索引。
  • 创建索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
PUT /test?pretty
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 2
},
"mapping": {
"test": {
"properties": {
"price": {
"type": "text"
},
"color": {
"type": "text"
},
"make": {
"type": "text"
},
"sold": {
"type": "text"
}
}
}
}
}
  • 新增数据

    批量插入部分数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
post /test/test/_bulk
{ "index": {}}
{ "price" : 10000, "color" : "red", "make" : "honda", "sold" : "2014-10-28" }
{ "index": {}}
{ "price" : 20000, "color" : "red", "make" : "honda", "sold" : "2014-11-05" }
{ "index": {}}
{ "price" : 30000, "color" : "green", "make" : "ford", "sold" : "2014-05-18" }
{ "index": {}}
{ "price" : 15000, "color" : "blue", "make" : "toyota", "sold" : "2014-07-02" }
{ "index": {}}
{ "price" : 12000, "color" : "green", "make" : "toyota", "sold" : "2014-08-19" }
{ "index": {}}
{ "price" : 20000, "color" : "red", "make" : "honda", "sold" : "2014-11-05" }
{ "index": {}}
{ "price" : 80000, "color" : "red", "make" : "bmw", "sold" : "2014-01-01" }
{ "index": {}}
{ "price" : 25000, "color" : "blue", "make" : "ford", "sold" : "2014-02-12" }

了解聚合概念

  • 桶(Buckets)

    满足特定条件的文档的集合

  • 指标(Metrics)

    对桶内的文档进行统计计算。

      聚合是由桶和指标组成的。 聚合可能只有一个桶,可能只有一个指标,或者可能两个都有。也有可能有一些桶嵌套在其他桶里面。例如,我们可以通过所属国家来划分文档(桶),然后计算每个国家的平均薪酬(指标)。
    
    另外聚合计算是实时的。一旦文档可以被搜到,它就能被聚合。这也就意味着我们可以直接将聚合的结果源源不断的传入前端,让其动态显示变化。
    
     5.x后对排序,聚合这些操作用单独的数据结构(fielddata)缓存到内存里了,需要单独开启。对哪个字段聚合就要开启哪个字段。比如我就开启了color字段,方便后续的聚合。
    
1
2
3
4
5
6
7
8
9
PUT test/_mapping/test/
{
"properties": {
"color": {
"type": "text",
"fielddata": true
}
}
}

根据颜色分组,参考mysql的groupby

1
2
3
4
5
6
7
8
9
10
11
GET /cars/transactions/_search
{
"size" : 0,
"aggs" : {
"popular_colors" : {
"terms" : {
"field" : "color"
}
}
}
}

核心结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"aggregations" : {
"popular_colors" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "red",
"doc_count" : 4
},
{
"key" : "blue",
"doc_count" : 2
},
{
"key" : "green",
"doc_count" : 2
}
]
}

对桶内的数据计算一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GET /test/test/_search
{
"size" : 0,
"aggs": {
"colors": {
"terms": {
"field": "color"
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
"aggregations" : {
"colors" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "red",
"doc_count" : 4,
"avg_price" : {
"value" : 32500.0
}
},
{
"key" : "blue",
"doc_count" : 2,
"avg_price" : {
"value" : 20000.0
}
},
{
"key" : "green",
"doc_count" : 2,
"avg_price" : {
"value" : 21000.0
}
}
]
}

希望求出每个颜色里面制造厂商的分布情况,那就里面在嵌套一个桶。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
GET /test/test/_search
{
"size" : 0,
"aggs": {
"colors": {
"terms": {
"field": "color"
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
},
"make": {
"terms": {
"field": "make"
}
}
}
}
}
}

结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
"aggregations" : {
"colors" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "red",
"doc_count" : 4,
"avg_price" : {
"value" : 32500.0
},
"make" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "honda",
"doc_count" : 3
},
{
"key" : "bmw",
"doc_count" : 1
}
]
}
},
......

条形图

这个关键字histogram,这个关键字要求字段必须是数值类型,然后interval定义了区间,然后可以在加几个指标,来获取这个区间的信息,比如最大值,最小值,和,平均值巴拉巴拉的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET /test/test/_search
{
"size" : 0,
"aggs":{
"pricesss":{
"histogram":{
"field": "price",
"interval": 20000
},
"aggs":{
"revenue": {
"max": {
"field" : "price"
}
}
}
}
}
}

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
"aggregations" : {
"pricesss" : {
"buckets" : [
{
"key" : 0.0,
"doc_count" : 3,
"revenue" : {
"value" : 15000.0
}
},
{
"key" : 20000.0,
"doc_count" : 4,
"revenue" : {
"value" : 30000.0
}
},
{
"key" : 40000.0,
"doc_count" : 0,
"revenue" : {
"value" : null
}
},
{
"key" : 60000.0,
"doc_count" : 0,
"revenue" : {
"value" : null
}
},
{
"key" : 80000.0,
"doc_count" : 1,
"revenue" : {
"value" : 80000.0
}
}
]
}

根据时间来聚合数据,首先命名为sales,然后使用关键字date_histogram,选择字段,和时间粒度即可。中间没有的月份也会显示,只是数目为0。

1
2
3
4
5
6
7
8
9
10
11
12
13
GET /test/test/_search
{
"size" : 0,
"aggs": {
"sales": {
"date_histogram": {
"field": "sold",
"interval": "year",
"format": "yyyy-MM-dd"
}
}
}
}

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"aggregations" : {
"sales" : {
"buckets" : [
{
"key_as_string" : "2014-01-01",
"key" : 1388534400000,
"doc_count" : 1
},
{
"key_as_string" : "2014-02-01",
"key" : 1391212800000,
"doc_count" : 1
},
{
"key_as_string" : "2014-03-01",
"key" : 1393632000000,
"doc_count" : 0
},
{
"key_as_string" : "2014-04-01",
"key" : 1396310400000,
"doc_count" : 0
},
......

另外由于elasticSearch版本差异太大,2.x版本date_histogramhistogram 时间聚合和数值聚合返回的buckets是没有空值的,也就是某个月份数据为0或者某个区间值为0,那么返回值里面是没有的(当然可以设置参数让其有),但是目前线上版本是6.x的,默认已经有空值了

下面来一个复杂的聚合,我来解释下其逻辑。

  • 首先根据每个季度创建一个桶,然后在每个桶里面在进行聚合计算。
  • 在季度每个桶里面第一个指标算出,这个季度一共卖了多少钱。
  • 在季度这个桶里面再根据制造厂商创建桶,同时计算出每个厂商卖了多少钱。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
GET /test/test/_search
{
"size" : 0,
"aggs": {
"sales": {
"date_histogram": {
"field": "sold",
"interval": "quarter",
"format": "yyyy-MM-dd"
},
"aggs": {
"per_make_sum": {
"terms": {
"field": "make"
},
"aggs": {
"sum_price": {
"sum": { "field": "price" }
}
}
},
"total_sum": {
"sum": { "field": "price" }
}
}
}
}
}

这是一个稍微复杂点的聚合逻辑,理解了他基本也就理解了全部的概念

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
"aggregations" : {
"sales" : {
"buckets" : [
{
"key_as_string" : "2014-01-01",
"key" : 1388534400000,
"doc_count" : 2,
"per_make_sum" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "bmw",
"doc_count" : 1,
"sum_price" : {
"value" : 80000.0
}
},
{
"key" : "ford",
"doc_count" : 1,
"sum_price" : {
"value" : 25000.0
}
}
]
},
"total_sum" : {
"value" : 105000.0
}
},
{
"key_as_string" : "2014-04-01",
"key" : 1396310400000,
"doc_count" : 1,
"per_make_sum" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "ford",
"doc_count" : 1,
"sum_price" : {
"value" : 30000.0
}
}
]
},
"total_sum" : {
"value" : 30000.0
}
},
{
"key_as_string" : "2014-07-01",
"key" : 1404172800000,
"doc_count" : 2,
"per_make_sum" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "toyota",
"doc_count" : 2,
"sum_price" : {
"value" : 27000.0
}
}
]
},
"total_sum" : {
"value" : 27000.0
}
},
......

我们可以将查询和聚合一起来使用,前面的聚合查询基本都相当于加了

1
2
3
4
5
"query" : {
"match" : {
"make" : "ford"
}
}

下面是个简单例子,是聚合和查询集合的例子。查出ford厂商的卖的车的颜色聚合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET /test/test/_search
{
"query" : {
"match" : {
"make" : "ford"
}
},
"aggs" : {
"colors" : {
"terms" : {
"field" : "color"
}
}
}
}

我们不仅可以在查询条件内做聚合,还可以全局聚合,同时使用,用法是关键字all

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
GET /test/test/_search
{
"size" : 0,
"query" : {
"match" : {
"make" : "ford"
}
},
"aggs" : {
"single_avg_price": {
"avg" : { "field" : "price" }
},
"all": {
"global" : {},
"aggs" : {
"avg_price": {
"avg" : { "field" : "price" }
}

}
}
}
}

结果如下:算出了查询值得平均值,还算出了全部文档的平均值。

1
2
3
4
5
6
7
8
9
10
11
"aggregations" : {
"all" : {
"doc_count" : 8,
"avg_price" : {
"value" : 26500.0
}
},
"single_avg_price" : {
"value" : 27500.0
}
}

聚合也可以和过滤一起使用。比如这个语句查出大于15000销售额的平均值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GET /test/test/_search
{

"query" : {
"constant_score": {
"filter": {
"range": {
"price": {
"gte": 15000
}
}
}
}
},
"aggs" : {
"single_avg_price": {
"avg" : { "field" : "price" }
}
}
}

结果如下:

1
2
3
4
5
"aggregations" : {
"single_avg_price" : {
"value" : 31666.666666666668
}
}

过滤桶,对聚合结果过滤。

先选出ford厂商的车,然后在这个结果里面再进行过滤选出上个月的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
GET /test/test/_search
{
"size" : 0,
"query":{
"match": {
"make": "ford"
}
},
"aggs":{
"recent_sales": {
"filter": {
"range": {
"sold": {
"from": "now-1M"
}
}
},
"aggs": {
"average_price":{
"avg": {
"field": "price"
}
}
}
}
}
}

有一种做法,先查询,然后聚合,再过滤一部分数据,这个过滤不能影响聚合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET /test/test/_search
{

"query": {
"match": {
"make": "ford"
}
},
"post_filter": {
"term" : {
"color" : "green"
}
},
"aggs" : {
"all_colors": {
"terms" : { "field" : "color" }
}
}
}

上面的聚合可以不断嵌套聚合。从而组合成更加复杂的结果

排序的话,下面这个技巧很有意思,可以参考了解下,业务中很有可能用的到。

  • 先根据颜色分组,然后算出每一组的平均值,然后再根据平均值排序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET /cars/transactions/_search
{
"size" : 0,
"aggs" : {
"colors" : {
"terms" : {
"field" : "color",
"order": {
"avg_price" : "asc"
}
},
"aggs": {
"avg_price": {
"avg": {"field": "price"}
}
}
}
}
}

统计唯一值

1
2
3
4
5
6
7
8
9
10
11
GET /test/test/_search
{
"size" : 0,
"aggs" : {
"distinct_colors" : {
"cardinality" : {
"field" : "color"
}
}
}
}

这个是统计颜色的种类,es使用了cardinality算法,也就是 HyperLogLog++ 算法做的,他不需要统计全部的数据,然后汇总计算,有误差,但是可以接受,性能可以。

如果统计唯一值得时候对速度要求很高,这块可以优化,参考:https://www.elastic.co/guide/cn/elasticsearch/guide/current/cardinality.html。就是给这些字段建立索引。